package com.gyy.socket;

import com.gyy.utils.IEEE754Utils;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

/**
 * @Author gmd
 * @Description java socket 实现modbusTCP的写功能
 * @Date 2022-02-04 10:56:03
 */
public class SocketTcpWrite {

    public static Socket socket;

    public static InputStream inputStream;

    public static OutputStream outputStream;


    /**
     * socket初始化，初始化socket、inputStream、outputStream
     */
    public static void initSocket(String host, int port) throws Exception {
        // 初始化socket
        socket = new Socket(host, port);
        // 初始化输入流
        inputStream = socket.getInputStream();
        // 初始化输出流
        outputStream = socket.getOutputStream();
    }


    /**
     * 释放资源（优雅关闭）
     */
    public static void release() throws Exception {
        // 关闭socket
        if (socket != null) {
            socket.close();
        }
        // 关闭输入流
        if (inputStream != null) {
            inputStream.close();
        }
        // 关闭输出流
        if (outputStream != null) {
            outputStream.close();
        }
    }


    /**
     * 写入单个线圈Coil状态值
     *
     * @param slaveId 从机ID，0-255
     * @param address 数据写入地址，下标从0开始
     * @param value   状态值
     */
    public static void writeCoil(int slaveId, int address, boolean value) throws Exception {
        // 先初始化发送报文全部为0
        byte[] sendMsg = new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
        // 设置发送报文数据的长度
        sendMsg[5] = 0x06;
        // 设置从机ID
        sendMsg[6] = (byte) slaveId;
        // 设置功能码为写入单个线圈状态
        sendMsg[7] = 0x05;
        // 设置数据写入地址
        if (address > 255) {
            sendMsg[8] = (byte) (address - 255);
            sendMsg[9] = (byte) 0xFF;
        } else {
            sendMsg[9] = (byte) address;
        }
        // 设置写入的值 '\uFF00'
        if (value) {
            sendMsg[10] = (byte) 0xFF;
            sendMsg[11] = 0x00;
        } else {
            sendMsg[10] = 0x00;
            sendMsg[11] = 0x00;
        }

        // 发送请求报文
        outputStream.write(sendMsg);
        outputStream.flush();

        // 初始化响应报文
        byte[] receiveMsg = new byte[12];
        // 读取响应报文，并写入数据
        inputStream.read(receiveMsg);

        // 判断报文是否合法，合法报文再做处理
        if (checkReceive(receiveMsg)) {
            System.out.println("线圈（0x01）单个数据写入响应报文，写入地址为：" + (receiveMsg[8]+receiveMsg[9]) + "，状态值为：" + (receiveMsg[10] == -1 ? true : false));
        }

    }


    /**
     * 写入多个线圈Coil状态值
     *
     * @param slaveId 从机ID，0-255
     * @param address 数据写入地址，下标从0开始
     * @param values   状态值数组
     */
    public static void writeCoils(int slaveId, int address, boolean[] values) throws Exception {
        // 先初始化发送报文长度并全部设置为0，初始发送报文的长度和状态值数组的长度有关，8个一组
        byte[] sendMsg = new byte[13 + (values.length + 7) / 8];
        // 设置发送报文数据的长度
        sendMsg[5] = (byte)( 7 + (values.length + 7) / 8);
        // 设置从机ID
        sendMsg[6] = (byte) slaveId;
        // 设置功能码为写入多个线圈状态
        sendMsg[7] = 0x0F;
        // 设置数据写入起始地址
        if (address > 255) {
            sendMsg[8] = (byte) (address - 255);
            sendMsg[9] = (byte) 0xFF;
        } else {
            sendMsg[9] = (byte) address;
        }
        // 设置数据写入结束地址
        if (address + values.length > 255) {
            sendMsg[10] = (byte) (address + values.length - 255);
            sendMsg[11] = (byte) 0xFF;
        } else {
            sendMsg[11] = (byte) (address + values.length);
        }

        // 根据状态值数组生成反向的二进制的字符串
        StringBuilder valueBits = new StringBuilder();
        for (boolean ele : values) {
            valueBits.insert(0, ele ? "1" : "0");
        }
        // 二进制字符串长度必须是8的倍数，不足则开头补0，因为这个二进制数组是反向的
        while (valueBits.length() % 8 != 0){
            valueBits.insert(0, "0");
        }
        // 需要写入的数据在发送报文的开始位置
        int index = 13;
        while (valueBits.length() > 0){
            // 这个字段保存写入数据的个数
            sendMsg[12] ++;
            // 从低位（右）开始，每8位二进制转成byte，顺序保存到数组中
            sendMsg[index++] = (byte) Integer.parseInt(valueBits.substring(valueBits.length()-8), 2);
            // 删除末尾8位二进制
            valueBits.delete(valueBits.length()-8, valueBits.length());
        }

        // 发送请求报文
        outputStream.write(sendMsg);
        outputStream.flush();

        // 初始化响应报文
        byte[] receiveMsg = new byte[12];
        // 读取响应报文，并写入数据
        inputStream.read(receiveMsg);

        // 判断报文是否合法，合法报文再做处理
        if (checkReceive(receiveMsg)) {
            System.out.println("线圈（0x01）多个数据写入响应报文，写入起始地址为：" + (receiveMsg[8]+receiveMsg[9]) + "，个数为：" + (receiveMsg[10]+receiveMsg[11]));
        }

    }


    /**
     * 写入单个保持寄存器HoldingRegister数据
     * 注意：单个写入，只能是int类型。浮点数写入必须是多个写入。当然，可以通过一定的规则转换，将浮点数转换成多个整数形式
     * @param slaveId 从机ID，0-255
     * @param address 数据写入地址，下标从0开始
     * @param value   数据值
     */
    public static void writeRegister(int slaveId, int address, int value) throws Exception {
        // 先初始化发送报文全部为0
        byte[] sendMsg = new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
        // 设置发送报文数据的长度
        sendMsg[5] = 0x06;
        // 设置从机ID
        sendMsg[6] = (byte) slaveId;
        // 设置功能码为写入单个寄存器数据
        sendMsg[7] = 0x06;
        // 设置数据写入地址
        if (address > 255) {
            sendMsg[8] = (byte) (address - 255);
            sendMsg[9] = (byte) 0xFF;
        } else {
            sendMsg[9] = (byte) address;
        }
        // 设置写入的值
        String data = String.format("%04X", value);
        sendMsg[10] = Byte.parseByte(data.substring(0, 2), 16);
        sendMsg[11] = Byte.parseByte(data.substring(2, 4), 16);

        // 发送请求报文
        outputStream.write(sendMsg);
        outputStream.flush();

        // 初始化响应报文
        byte[] receiveMsg = new byte[12];
        // 读取响应报文，并写入数据
        inputStream.read(receiveMsg);

        // 判断报文是否合法，合法报文再做处理
        if (checkReceive(receiveMsg)) {
            System.out.println("保持寄存器（0x03）单个数据写入响应报文，写入地址为：" + (receiveMsg[8]+receiveMsg[9]) + "，数据为：" + (receiveMsg[10]*256+receiveMsg[11]));
        }

    }


    /**
     * 校验响应报文是否出错，如果出错则打印报错信息
     *
     * @return true 报文正常   false 报文错误
     */
    public static boolean checkReceive(byte[] message) {
        if ((int) message[7] < 0) {
            switch (message[8]) {
                case 0x01:
                    System.out.print("异常码：01（非法功能码，从站设备不支持此功能码）。");
                    break;
                case 0x02:
                    System.out.print("异常码：02（非法数据地址，指定的数据地址在从站设备中不存在）。");
                    break;
                case 0x03:
                    System.out.print("异常码：03（非法数据值，指定的数据超过范围或者不允许使用）。");
                    break;
                case 0x04:
                    System.out.print("异常码：04（从站设备故障，从站设备处理响应的过程中，出现未知错误等）。");
                    break;
                default:
                    System.out.print("程序异常。");

            }
            System.out.println("响应报文为：" + IEEE754Utils.bitsToHexStr(message));
            return false;

        } else {
            return true;
        }
    }


    public static void main(String[] args) throws Exception {
        initSocket("127.0.0.1", 502);

        // 写入单个线圈数据
        //writeCoil(1, 0, true);

        // 写入多个线圈数据
        //writeCoils(1, 0, new boolean[]{true, false, true, false, false, true, false, false, false, true});

        // 写入单个保持寄存器数据
        writeRegister(3, 1, 123);

        release();
    }


}
